comfyui-mcp 0.52.119 → 0.52.121
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.
package/README.md
CHANGED
|
@@ -862,7 +862,8 @@ plugin/
|
|
|
862
862
|
job-complete-notify.mjs # Job completion notification via temp files
|
|
863
863
|
scripts/ # Background scripts
|
|
864
864
|
monitor-progress.mjs # Real-time WebSocket progress monitor
|
|
865
|
-
launch-server.mjs # MCP server launcher —
|
|
865
|
+
launch-server.mjs # MCP server launcher — global install if present, else npx with a
|
|
866
|
+
# cold-start handshake rescue so a first run cannot time out (#1447)
|
|
866
867
|
```
|
|
867
868
|
|
|
868
869
|
---
|
|
@@ -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,166 @@ 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
|
+
/** Budget for the gate's own detail probe — the panel_query_graph `max_chars`
|
|
4950
|
+
* ceiling, not the default.
|
|
4951
|
+
*
|
|
4952
|
+
* This is a PINPOINT read: one id, `limit: 1`, one row. The survey cap exists to
|
|
4953
|
+
* keep a 200-node listing small and has nothing to protect here. Leaving it at the
|
|
4954
|
+
* default is what makes the gate miss the case it was written for: the panel caps a
|
|
4955
|
+
* detail row and, past a point, degrades it to an `{id, type, title}` stub — which
|
|
4956
|
+
* drops `inputs`, so the parse cannot prove the shape and the write falls open.
|
|
4957
|
+
* The node most likely to overflow that budget is the node already holding a long
|
|
4958
|
+
* prompt, i.e. exactly the #2299 report ("<any long prompt>"). */
|
|
4959
|
+
const DYNAMIC_COMBO_PROBE_MAX_CHARS = 60000;
|
|
4960
|
+
function dynamicComboSubWidgetRefusal(nodeType, parentInput, widget) {
|
|
4961
|
+
return fail(`panel_set_widget cannot set dynamic-combo sub-widget "${widget}" on ${nodeType}. ` +
|
|
4962
|
+
`The parent input "${parentInput}" is ${COMFY_DYNAMICCOMBO_V3} and "${widget}" is a ` +
|
|
4963
|
+
`${DYNAMIC_COMBO_REFUSED_CHILD_TYPE} child; its frontend ` +
|
|
4964
|
+
`serializer can re-materialize the combo and overwrite widget.value after a ` +
|
|
4965
|
+
`successful write and immediate readback, so panel_run could queue an empty or ` +
|
|
4966
|
+
`stale value. Drive the child input by link instead: add a ` +
|
|
4967
|
+
`PrimitiveStringMultiline, set its STRING widget, then panel_connect its ` +
|
|
4968
|
+
`STRING output to this node's "${widget}" input. Set the parent "${parentInput}" ` +
|
|
4969
|
+
`only to choose the combo option. No graph_set_widget was dispatched; do not ` +
|
|
4970
|
+
`retry this dotted child write.`);
|
|
4971
|
+
}
|
|
4972
|
+
/** Refuse a dotted widget only when the live detail row proves BOTH that its
|
|
4973
|
+
* prefix is a COMFY_DYNAMICCOMBO_V3 input and that the addressed child is a
|
|
4974
|
+
* STRING (see {@link DYNAMIC_COMBO_REFUSED_CHILD_TYPE} for why the child type is
|
|
4975
|
+
* load-bearing rather than the parent type alone).
|
|
4976
|
+
* Ordinary composite widgets (for example rgthree `lora_1.on`) and ordinary
|
|
4977
|
+
* dotted STRING names remain on the normal setter path. An unreadable detail
|
|
4978
|
+
* probe is not evidence of a dynamic combo, so it falls through to the panel's
|
|
4979
|
+
* existing validation rather than broadening this refusal. */
|
|
4980
|
+
async function refuseDynamicComboSubWidgetWrite(ctx, nodeId, widget) {
|
|
4981
|
+
const dot = widget.indexOf(".");
|
|
4982
|
+
if (dot <= 0 || dot === widget.length - 1)
|
|
4983
|
+
return null;
|
|
4984
|
+
const probe = await ctx.call({
|
|
4985
|
+
cmd: "graph_query",
|
|
4986
|
+
ids: [nodeId],
|
|
4987
|
+
fields: "detail",
|
|
4988
|
+
limit: 1,
|
|
4989
|
+
max_chars: DYNAMIC_COMBO_PROBE_MAX_CHARS,
|
|
4990
|
+
});
|
|
4991
|
+
if (probe.isError)
|
|
4992
|
+
return null;
|
|
4993
|
+
// An unreadable or truncated probe is NOT evidence of a dynamic combo, so it
|
|
4994
|
+
// falls open — the same discipline as refuseAnimaRegionalPromptWrite. Failing
|
|
4995
|
+
// CLOSED here would refuse every dotted widget on every node whenever a probe
|
|
4996
|
+
// hiccups (rgthree `lora_N.*`, promoted subgraph paths, JSON composite fields),
|
|
4997
|
+
// which is a far larger blast radius than the residual it would close. The
|
|
4998
|
+
// residual is a slice of the PRE-EXISTING bug, not one this gate introduces:
|
|
4999
|
+
// without the gate every one of these writes is a false success. Shrinking that
|
|
5000
|
+
// slice is what the generous probe budget above is for. Closing it entirely
|
|
5001
|
+
// needs #2299's option 2 — an honest "written, not confirmed persistent" receipt
|
|
5002
|
+
// — which changes the success contract of the tool and is not this guard's call.
|
|
5003
|
+
const detail = parseVerifiedQueriedNodeDetail(parseToolResultJson(probe));
|
|
5004
|
+
const requestedId = canonicalQueriedNodeId(nodeId);
|
|
5005
|
+
if (!detail || !requestedId || detail.id !== requestedId)
|
|
5006
|
+
return null;
|
|
5007
|
+
const parentInput = widget.slice(0, dot);
|
|
5008
|
+
const childInput = detail.inputs.find((input) => input &&
|
|
5009
|
+
typeof input === "object" &&
|
|
5010
|
+
!Array.isArray(input) &&
|
|
5011
|
+
input.name === widget);
|
|
5012
|
+
const childExists = childInput !== undefined ||
|
|
5013
|
+
(detail.widgets !== null && Object.prototype.hasOwnProperty.call(detail.widgets, widget));
|
|
5014
|
+
if (!childExists)
|
|
5015
|
+
return null;
|
|
5016
|
+
const parent = detail.inputs.find((input) => input &&
|
|
5017
|
+
typeof input === "object" &&
|
|
5018
|
+
!Array.isArray(input) &&
|
|
5019
|
+
input.name === parentInput);
|
|
5020
|
+
if (!parent || typeof parent !== "object" || Array.isArray(parent))
|
|
5021
|
+
return null;
|
|
5022
|
+
if (parent.type !== COMFY_DYNAMICCOMBO_V3)
|
|
5023
|
+
return null;
|
|
5024
|
+
// A dynamic-combo parent is not on its own the #2299 shape. Only a child the
|
|
5025
|
+
// probe PROVES is STRING is refused; a COMBO/INT/FLOAT/BOOLEAN child, or one
|
|
5026
|
+
// whose declared type this row does not carry, keeps the normal setter path.
|
|
5027
|
+
const childType = childInput && typeof childInput === "object" && !Array.isArray(childInput)
|
|
5028
|
+
? childInput.type
|
|
5029
|
+
: undefined;
|
|
5030
|
+
if (childType !== DYNAMIC_COMBO_REFUSED_CHILD_TYPE)
|
|
5031
|
+
return null;
|
|
5032
|
+
return dynamicComboSubWidgetRefusal(detail.type, parentInput, widget);
|
|
5033
|
+
}
|
|
4873
5034
|
/** Refuse the known LC123 regional-canvas prompt widgets. Identity is read
|
|
4874
5035
|
* from a one-node `graph_query`; anything else (query failed, type unreadable,
|
|
4875
5036
|
* some other node that happens to own `negative_prompt`) is fail-open. */
|
|
@@ -12686,7 +12847,17 @@ function withRetryToken(d) {
|
|
|
12686
12847
|
*/
|
|
12687
12848
|
export function buildPanelToolDefs() {
|
|
12688
12849
|
// Local helper so each def reads like the original `tool(...)` call.
|
|
12689
|
-
const def = (name, description, schema, handler) => ({
|
|
12850
|
+
const def = (name, description, schema, handler) => ({
|
|
12851
|
+
name,
|
|
12852
|
+
// Keep the long historical setter description readable at its call site;
|
|
12853
|
+
// #2299's dynamic-combo warning is appended here so both registrations
|
|
12854
|
+
// expose the same actionable workaround.
|
|
12855
|
+
description: name === "panel_set_widget"
|
|
12856
|
+
? `${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.`
|
|
12857
|
+
: description,
|
|
12858
|
+
schema,
|
|
12859
|
+
handler,
|
|
12860
|
+
});
|
|
12690
12861
|
const capturePanelKitchenTarget = (ctx) => {
|
|
12691
12862
|
const base = getComfyUIBaseUrl().replace(/\/+$/, "");
|
|
12692
12863
|
const observed = ctx.bridge.tabServerOrigin?.(ctx.tabId);
|
|
@@ -13584,6 +13755,9 @@ export function buildPanelToolDefs() {
|
|
|
13584
13755
|
const blocked = await refuseAnimaRegionalPromptWrite(ctx, args.node_id, args.widget);
|
|
13585
13756
|
if (blocked)
|
|
13586
13757
|
return blocked;
|
|
13758
|
+
const dynamicComboBlocked = await refuseDynamicComboSubWidgetWrite(ctx, args.node_id, args.widget);
|
|
13759
|
+
if (dynamicComboBlocked)
|
|
13760
|
+
return dynamicComboBlocked;
|
|
13587
13761
|
const daSiWaBlocked = await refuseDaSiWaStackWrite(ctx, args.node_id, args.widget);
|
|
13588
13762
|
if (daSiWaBlocked)
|
|
13589
13763
|
return daSiWaBlocked;
|
|
@@ -13791,6 +13965,29 @@ export function buildPanelToolDefs() {
|
|
|
13791
13965
|
}
|
|
13792
13966
|
innerExpectedNodeType = innerIdentity.type;
|
|
13793
13967
|
}
|
|
13968
|
+
// #2299 — every pre-write guard above probed the OUTER scope, but this retry
|
|
13969
|
+
// writes a DIFFERENT node: the promoted inner one. For a dynamic-combo child
|
|
13970
|
+
// that is exactly the false success the guard exists to stop. The outer probe
|
|
13971
|
+
// cannot see it and correctly falls open — the container exposes the promoted
|
|
13972
|
+
// child but not the `model` PARENT, so the COMFY_DYNAMICCOMBO_V3 half of the
|
|
13973
|
+
// shape is unprovable there. Only the inner node carries both halves.
|
|
13974
|
+
//
|
|
13975
|
+
// The stack_data fence above does re-query after entering, but it is gated on
|
|
13976
|
+
// `expectedNodeType`, which is set only when the addressed widget IS stack_data
|
|
13977
|
+
// — so it never runs for any other widget. This is the same re-probe for the
|
|
13978
|
+
// dotted-child case, and the canvas is already inside the subgraph here, so
|
|
13979
|
+
// `inner.innerNodeId` resolves.
|
|
13980
|
+
const innerDynamicComboBlocked = await refuseDynamicComboSubWidgetWrite(ctx, inner.innerNodeId, inner.widget);
|
|
13981
|
+
if (innerDynamicComboBlocked) {
|
|
13982
|
+
const exitedEarly = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
13983
|
+
return appendToolResultText(first, `
|
|
13984
|
+
|
|
13985
|
+
(The promoted inner node ${inner.innerNodeId} owns "${inner.widget}" as a ` +
|
|
13986
|
+
`dynamic-combo child; ${textOfToolResult(innerDynamicComboBlocked)} ` +
|
|
13987
|
+
`No inner graph_set_widget was dispatched.${exitedEarly.isError
|
|
13988
|
+
? ` panel_exit_subgraph also FAILED: ${textOfToolResult(exitedEarly)}`
|
|
13989
|
+
: ""})`);
|
|
13990
|
+
}
|
|
13794
13991
|
const written = await write(inner.innerNodeId, inner.widget, innerExpectedNodeType);
|
|
13795
13992
|
const exited = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
13796
13993
|
if (!written.isError) {
|