comfyui-mcp 0.52.119 → 0.52.120

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.
@@ -4724,6 +4724,7 @@ const ANIMA_REGIONAL_WIRED_PROMPT_INPUT = {
4724
4724
  };
4725
4725
  const DASIWA_STACK_WIDGET = "stack_data";
4726
4726
  const DASIWA_LTX2_LORA_LOADER = "DaSiWa_LTX2LoraLoader";
4727
+ const COMFY_DYNAMICCOMBO_V3 = "COMFY_DYNAMICCOMBO_V3";
4727
4728
  function isAnimaRegionalPromptWidget(widget) {
4728
4729
  return ANIMA_REGIONAL_PROMPT_WIDGETS.has(widget);
4729
4730
  }
@@ -4870,6 +4871,144 @@ function animaRegionalPromptRefusal(type, widget) {
4870
4871
  `APPLY / savePrompts() copies the textarea back over widget.value, so a success here would be a lie. ` +
4871
4872
  `${route} Do not retry panel_set_widget on this widget.`);
4872
4873
  }
4874
+ /** Strictly parse one detail row for the dynamic-combo child gate. A compact
4875
+ * response is intentionally not enough: this guard needs the live input names
4876
+ * and types, not just the node type. */
4877
+ function parseVerifiedQueriedNodeDetail(payload) {
4878
+ if (!payload)
4879
+ return null;
4880
+ if (Object.prototype.hasOwnProperty.call(payload, "truncated") &&
4881
+ payload.truncated !== false) {
4882
+ return null;
4883
+ }
4884
+ let row;
4885
+ if (Object.prototype.hasOwnProperty.call(payload, "nodes")) {
4886
+ if (!Array.isArray(payload.nodes) || payload.nodes.length !== 1)
4887
+ return null;
4888
+ row = payload.nodes[0];
4889
+ }
4890
+ else if (typeof payload.text === "string") {
4891
+ let headerSeen = false;
4892
+ for (const line of payload.text.split(/\r?\n/)) {
4893
+ const trimmed = line.trim();
4894
+ if (!trimmed)
4895
+ continue;
4896
+ if (!headerSeen && /^\d+ match\(es\) of \d+ in scope/.test(trimmed)) {
4897
+ headerSeen = true;
4898
+ continue;
4899
+ }
4900
+ if (!trimmed.startsWith("{")) {
4901
+ // Detail replies may carry a bounded clipping/truncation footer after
4902
+ // the one JSON row. It cannot establish a second node identity.
4903
+ if (row !== undefined)
4904
+ continue;
4905
+ return null;
4906
+ }
4907
+ if (row !== undefined)
4908
+ return null;
4909
+ try {
4910
+ row = JSON.parse(trimmed);
4911
+ }
4912
+ catch {
4913
+ return null;
4914
+ }
4915
+ headerSeen = true;
4916
+ }
4917
+ }
4918
+ if (!row || typeof row !== "object" || Array.isArray(row))
4919
+ return null;
4920
+ const record = row;
4921
+ const identity = parseQueriedNodeIdentityRow(record);
4922
+ if (!identity || !Array.isArray(record.inputs))
4923
+ return null;
4924
+ const widgets = record.widgets && typeof record.widgets === "object" && !Array.isArray(record.widgets)
4925
+ ? record.widgets
4926
+ : null;
4927
+ return { ...identity, inputs: record.inputs, widgets };
4928
+ }
4929
+ /** The ONLY dynamic-combo child type this refuses.
4930
+ *
4931
+ * #2299 measured exactly one reverting child — `model.prompt`, a STRING — and a
4932
+ * STRING child is the only one with a durable escape: it is exposed as a real
4933
+ * input socket, so a PrimitiveStringMultiline link survives the frontend's
4934
+ * re-serialize pass and wins at execution.
4935
+ *
4936
+ * Refusing the parent's whole child set instead would be a capability removal.
4937
+ * `src/services/api-nodes.ts` classifies a v3 dynamic combo's children as
4938
+ * INT | FLOAT | STRING | BOOLEAN | COMBO, and the Nano Banana 2 shape fixtured in
4939
+ * `src/__tests__/services/api-nodes.test.ts` reveals `model.aspect_ratio`,
4940
+ * `model.resolution` and `model.thinking_level` — all COMBO, and all REQUIRED:
4941
+ * the server 400s with `required_input_missing` when a dotted child is absent.
4942
+ * A refusal there would leave a required input with NO route, while naming a
4943
+ * remedy that cannot be carried out (a STRING output does not reach a COMBO
4944
+ * input). No non-STRING child has been measured reverting; if one ever is, it
4945
+ * needs an honest receipt ("written, not confirmed persistent"), not a refusal.
4946
+ *
4947
+ * So: prove STRING, or keep the normal setter path. */
4948
+ const DYNAMIC_COMBO_REFUSED_CHILD_TYPE = "STRING";
4949
+ function dynamicComboSubWidgetRefusal(nodeType, parentInput, widget) {
4950
+ return fail(`panel_set_widget cannot set dynamic-combo sub-widget "${widget}" on ${nodeType}. ` +
4951
+ `The parent input "${parentInput}" is ${COMFY_DYNAMICCOMBO_V3} and "${widget}" is a ` +
4952
+ `${DYNAMIC_COMBO_REFUSED_CHILD_TYPE} child; its frontend ` +
4953
+ `serializer can re-materialize the combo and overwrite widget.value after a ` +
4954
+ `successful write and immediate readback, so panel_run could queue an empty or ` +
4955
+ `stale value. Drive the child input by link instead: add a ` +
4956
+ `PrimitiveStringMultiline, set its STRING widget, then panel_connect its ` +
4957
+ `STRING output to this node's "${widget}" input. Set the parent "${parentInput}" ` +
4958
+ `only to choose the combo option. No graph_set_widget was dispatched; do not ` +
4959
+ `retry this dotted child write.`);
4960
+ }
4961
+ /** Refuse a dotted widget only when the live detail row proves BOTH that its
4962
+ * prefix is a COMFY_DYNAMICCOMBO_V3 input and that the addressed child is a
4963
+ * STRING (see {@link DYNAMIC_COMBO_REFUSED_CHILD_TYPE} for why the child type is
4964
+ * load-bearing rather than the parent type alone).
4965
+ * Ordinary composite widgets (for example rgthree `lora_1.on`) and ordinary
4966
+ * dotted STRING names remain on the normal setter path. An unreadable detail
4967
+ * probe is not evidence of a dynamic combo, so it falls through to the panel's
4968
+ * existing validation rather than broadening this refusal. */
4969
+ async function refuseDynamicComboSubWidgetWrite(ctx, nodeId, widget) {
4970
+ const dot = widget.indexOf(".");
4971
+ if (dot <= 0 || dot === widget.length - 1)
4972
+ return null;
4973
+ const probe = await ctx.call({
4974
+ cmd: "graph_query",
4975
+ ids: [nodeId],
4976
+ fields: "detail",
4977
+ limit: 1,
4978
+ });
4979
+ if (probe.isError)
4980
+ return null;
4981
+ const detail = parseVerifiedQueriedNodeDetail(parseToolResultJson(probe));
4982
+ const requestedId = canonicalQueriedNodeId(nodeId);
4983
+ if (!detail || !requestedId || detail.id !== requestedId)
4984
+ return null;
4985
+ const parentInput = widget.slice(0, dot);
4986
+ const childInput = detail.inputs.find((input) => input &&
4987
+ typeof input === "object" &&
4988
+ !Array.isArray(input) &&
4989
+ input.name === widget);
4990
+ const childExists = childInput !== undefined ||
4991
+ (detail.widgets !== null && Object.prototype.hasOwnProperty.call(detail.widgets, widget));
4992
+ if (!childExists)
4993
+ return null;
4994
+ const parent = detail.inputs.find((input) => input &&
4995
+ typeof input === "object" &&
4996
+ !Array.isArray(input) &&
4997
+ input.name === parentInput);
4998
+ if (!parent || typeof parent !== "object" || Array.isArray(parent))
4999
+ return null;
5000
+ if (parent.type !== COMFY_DYNAMICCOMBO_V3)
5001
+ return null;
5002
+ // A dynamic-combo parent is not on its own the #2299 shape. Only a child the
5003
+ // probe PROVES is STRING is refused; a COMBO/INT/FLOAT/BOOLEAN child, or one
5004
+ // whose declared type this row does not carry, keeps the normal setter path.
5005
+ const childType = childInput && typeof childInput === "object" && !Array.isArray(childInput)
5006
+ ? childInput.type
5007
+ : undefined;
5008
+ if (childType !== DYNAMIC_COMBO_REFUSED_CHILD_TYPE)
5009
+ return null;
5010
+ return dynamicComboSubWidgetRefusal(detail.type, parentInput, widget);
5011
+ }
4873
5012
  /** Refuse the known LC123 regional-canvas prompt widgets. Identity is read
4874
5013
  * from a one-node `graph_query`; anything else (query failed, type unreadable,
4875
5014
  * some other node that happens to own `negative_prompt`) is fail-open. */
@@ -12686,7 +12825,17 @@ function withRetryToken(d) {
12686
12825
  */
12687
12826
  export function buildPanelToolDefs() {
12688
12827
  // Local helper so each def reads like the original `tool(...)` call.
12689
- const def = (name, description, schema, handler) => ({ name, description, schema, handler });
12828
+ const def = (name, description, schema, handler) => ({
12829
+ name,
12830
+ // Keep the long historical setter description readable at its call site;
12831
+ // #2299's dynamic-combo warning is appended here so both registrations
12832
+ // expose the same actionable workaround.
12833
+ description: name === "panel_set_widget"
12834
+ ? `${description} A dotted STRING child of a live COMFY_DYNAMICCOMBO_V3 input (for example model.prompt) is refused when the live detail proves both halves of that shape (non-STRING children such as Nano Banana 2's model.resolution stay writable): the frontend can reserialize the combo after immediate readback and panel_run may then use an empty/stale child. Drive the corresponding STRING child input with a PrimitiveStringMultiline link instead; set the parent only to choose the combo option.`
12835
+ : description,
12836
+ schema,
12837
+ handler,
12838
+ });
12690
12839
  const capturePanelKitchenTarget = (ctx) => {
12691
12840
  const base = getComfyUIBaseUrl().replace(/\/+$/, "");
12692
12841
  const observed = ctx.bridge.tabServerOrigin?.(ctx.tabId);
@@ -13584,6 +13733,9 @@ export function buildPanelToolDefs() {
13584
13733
  const blocked = await refuseAnimaRegionalPromptWrite(ctx, args.node_id, args.widget);
13585
13734
  if (blocked)
13586
13735
  return blocked;
13736
+ const dynamicComboBlocked = await refuseDynamicComboSubWidgetWrite(ctx, args.node_id, args.widget);
13737
+ if (dynamicComboBlocked)
13738
+ return dynamicComboBlocked;
13587
13739
  const daSiWaBlocked = await refuseDaSiWaStackWrite(ctx, args.node_id, args.widget);
13588
13740
  if (daSiWaBlocked)
13589
13741
  return daSiWaBlocked;
@@ -13791,6 +13943,29 @@ export function buildPanelToolDefs() {
13791
13943
  }
13792
13944
  innerExpectedNodeType = innerIdentity.type;
13793
13945
  }
13946
+ // #2299 — every pre-write guard above probed the OUTER scope, but this retry
13947
+ // writes a DIFFERENT node: the promoted inner one. For a dynamic-combo child
13948
+ // that is exactly the false success the guard exists to stop. The outer probe
13949
+ // cannot see it and correctly falls open — the container exposes the promoted
13950
+ // child but not the `model` PARENT, so the COMFY_DYNAMICCOMBO_V3 half of the
13951
+ // shape is unprovable there. Only the inner node carries both halves.
13952
+ //
13953
+ // The stack_data fence above does re-query after entering, but it is gated on
13954
+ // `expectedNodeType`, which is set only when the addressed widget IS stack_data
13955
+ // — so it never runs for any other widget. This is the same re-probe for the
13956
+ // dotted-child case, and the canvas is already inside the subgraph here, so
13957
+ // `inner.innerNodeId` resolves.
13958
+ const innerDynamicComboBlocked = await refuseDynamicComboSubWidgetWrite(ctx, inner.innerNodeId, inner.widget);
13959
+ if (innerDynamicComboBlocked) {
13960
+ const exitedEarly = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
13961
+ return appendToolResultText(first, `
13962
+
13963
+ (The promoted inner node ${inner.innerNodeId} owns "${inner.widget}" as a ` +
13964
+ `dynamic-combo child; ${textOfToolResult(innerDynamicComboBlocked)} ` +
13965
+ `No inner graph_set_widget was dispatched.${exitedEarly.isError
13966
+ ? ` panel_exit_subgraph also FAILED: ${textOfToolResult(exitedEarly)}`
13967
+ : ""})`);
13968
+ }
13794
13969
  const written = await write(inner.innerNodeId, inner.widget, innerExpectedNodeType);
13795
13970
  const exited = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
13796
13971
  if (!written.isError) {