comfyui-mcp 0.50.64 → 0.50.66
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
|
@@ -797,7 +797,6 @@ src/
|
|
|
797
797
|
model-resolver.ts # HuggingFace search, local models, downloads
|
|
798
798
|
generation-tracker.ts # SQLite generation log, settings dedup, stats
|
|
799
799
|
file-hasher.ts # SHA256 hashing of .safetensors with cache
|
|
800
|
-
civitai-lookup.ts # CivitAI API lookup by content hash
|
|
801
800
|
workflow-settings-extractor.ts # Extract settings from workflow JSON
|
|
802
801
|
process-control.ts # Stop, start, restart ComfyUI process
|
|
803
802
|
registry-client.ts # ComfyUI Registry API
|
|
@@ -714,7 +714,10 @@ function isLoopbackHostName(host) {
|
|
|
714
714
|
}
|
|
715
715
|
/** The scheme://host:port origin of a URL (default ports made explicit), or null if
|
|
716
716
|
* unparseable. Loopback hosts canonicalize to their FAMILY loopback (v4 → 127.0.0.1,
|
|
717
|
-
* v6 → ::1) — so
|
|
717
|
+
* v6 → ::1) — so 127.0.0.1/0.0.0.0 compare equal, and ::1/:: compare equal. NOTE:
|
|
718
|
+
* `localhost` is deliberately NOT folded (loopbackFamily returns null for it), so it
|
|
719
|
+
* compares UNEQUAL to a concrete literal — callers that would harm a user by treating
|
|
720
|
+
* that as proof of difference must degrade it to "unknown" themselves (#1233),
|
|
718
721
|
* but a v4 host and a v6 host DIFFER (they may be different instances). Ports differ. */
|
|
719
722
|
function httpOriginOf(rawUrl) {
|
|
720
723
|
try {
|
|
@@ -1002,14 +1005,50 @@ async function probeDeclineRecovery(base, windowMs, intervalMs, probeTimeoutMs,
|
|
|
1002
1005
|
*
|
|
1003
1006
|
* Pure, so the three-way decision is testable without a live panel.
|
|
1004
1007
|
*/
|
|
1005
|
-
export function restartTimeoutFallbackAdvice({ headlessBase, panelBase, }) {
|
|
1008
|
+
export function restartTimeoutFallbackAdvice({ headlessBase, panelBase, observedOrigin = null, }) {
|
|
1006
1009
|
if (panelBase != null && sameHttpBase(headlessBase, panelBase)) {
|
|
1007
1010
|
return "or use restart_comfyui to restart the server directly without a panel card.";
|
|
1008
1011
|
}
|
|
1009
|
-
|
|
1012
|
+
// Proven different: either the proof resolved a base that differs, or the observed
|
|
1013
|
+
// origin — which the browser sets and page JS cannot forge — differs outright.
|
|
1014
|
+
// ORIGIN-only compare for the raw Origin, deliberately. `tabServerOrigin` is pathless
|
|
1015
|
+
// (scheme+host+port — the browser sends no path on a WS upgrade), while `headlessBase`
|
|
1016
|
+
// may carry a basePath mount such as :8188/comfy. A path-AWARE compare therefore reads
|
|
1017
|
+
// a correctly-mounted install as a mismatch and fires the strong warning at a server
|
|
1018
|
+
// that IS the right one — the cry-wolf failure this branch exists to avoid, which is
|
|
1019
|
+
// how it was found. A pathless Origin can prove a different host:port; it cannot prove
|
|
1020
|
+
// a different path, so it is only ever used for the claim it can actually support.
|
|
1021
|
+
// (captureRebootHealthBase fails closed on the same ambiguity for the same reason.)
|
|
1022
|
+
// #1233 (codex) — a DNS-ambiguous `localhost` on EITHER side is NOT proof of a
|
|
1023
|
+
// different server. loopbackFamily() deliberately excludes it (a coordinator P0: it
|
|
1024
|
+
// can resolve to either family, or to something else entirely), so `localhost:8188`
|
|
1025
|
+
// vs `127.0.0.1:8188` canonicalizes unequal and would fire the strong warning at what
|
|
1026
|
+
// is very likely the same instance. Absence of proof is not proof of difference, so
|
|
1027
|
+
// an ambiguous host degrades to UNPROVEN — the same direction captureRebootHealthBase
|
|
1028
|
+
// takes for the same input.
|
|
1029
|
+
const dnsAmbiguous = (u) => {
|
|
1030
|
+
if (!u)
|
|
1031
|
+
return false;
|
|
1032
|
+
try {
|
|
1033
|
+
return new URL(u).hostname.toLowerCase().replace(/^\[|\]$/g, "") === "localhost";
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
const originMismatch = observedOrigin != null &&
|
|
1040
|
+
!dnsAmbiguous(observedOrigin) &&
|
|
1041
|
+
!dnsAmbiguous(headlessBase) &&
|
|
1042
|
+
!sameHttpOrigin(headlessBase, observedOrigin)
|
|
1043
|
+
? observedOrigin
|
|
1044
|
+
: null;
|
|
1045
|
+
const proven = panelBase ?? originMismatch;
|
|
1046
|
+
if (proven != null) {
|
|
1010
1047
|
return (`Do NOT reach for restart_comfyui here without checking: it targets ${headlessBase}, ` +
|
|
1011
|
-
`while this panel is running inside ${
|
|
1012
|
-
"different server than the one you have been working on
|
|
1048
|
+
`while this panel is running inside ${proven}. Restarting the first would hit a ` +
|
|
1049
|
+
"different server than the one you have been working on. That may find nothing there " +
|
|
1050
|
+
"at all — or it may SUCCEED and take down a ComfyUI you did not mean to touch, which " +
|
|
1051
|
+
"on a shared instance is the worse outcome.");
|
|
1013
1052
|
}
|
|
1014
1053
|
return (`or use restart_comfyui, which restarts ${headlessBase} directly without a panel ` +
|
|
1015
1054
|
"card — check that this is the same ComfyUI you have been working on, since I " +
|
|
@@ -1432,6 +1471,102 @@ async function confirmEmptyOutline(ctx, res, reread) {
|
|
|
1432
1471
|
* a longer poll would buy rarer restores at the expense of every empty read.
|
|
1433
1472
|
*/
|
|
1434
1473
|
const EMPTY_OUTLINE_RECHECK_STEPS_MS = [250, 400, 550];
|
|
1474
|
+
/**
|
|
1475
|
+
* Enforce `max_chars` when the PANEL could not (#1203).
|
|
1476
|
+
*
|
|
1477
|
+
* A panel older than the budget protocol ignores `max_chars` and returns the
|
|
1478
|
+
* whole outline. The orchestrator already noticed — that is what
|
|
1479
|
+
* `markBudgetIgnored` is for — and then forwarded the unbounded reply anyway,
|
|
1480
|
+
* with a note explaining that the bound the caller had explicitly set did not
|
|
1481
|
+
* apply. A 137-node outline arrived that way, which is the exact context flood
|
|
1482
|
+
* `max_chars` exists to prevent, and the disclosure came AFTER the cost was
|
|
1483
|
+
* already paid. A warning is not a bound.
|
|
1484
|
+
*
|
|
1485
|
+
* WHY REFUSE RATHER THAN TRUNCATE. This tool's contract is that coverage is
|
|
1486
|
+
* never traded away: over budget the outline sheds RESOLUTION — widget values,
|
|
1487
|
+
* then titles, then a per-group summary — so whatever comes back still describes
|
|
1488
|
+
* the whole graph, and if even the floor will not fit it returns NO outline
|
|
1489
|
+
* rather than a partial one that reads as complete. Cutting the rendered text at
|
|
1490
|
+
* a character count would break exactly that guarantee: the result would look
|
|
1491
|
+
* like a finished outline that simply ends, and a caller acting on it would
|
|
1492
|
+
* believe the graph stops where the string does — the #1184 failure again, with
|
|
1493
|
+
* the truncation invisible instead of announced.
|
|
1494
|
+
*
|
|
1495
|
+
* Shedding resolution is the panel's job and needs the graph, not its rendering.
|
|
1496
|
+
* The orchestrator has only the rendered text, so the honest move is the one the
|
|
1497
|
+
* panel itself makes at its floor: return no outline, and say precisely what
|
|
1498
|
+
* happened, how big the outline actually was, and which of the three levers
|
|
1499
|
+
* (raise the bound, update the panel, scope the read) will work.
|
|
1500
|
+
*
|
|
1501
|
+
* The counts and `viewing` survive — they are the part of the reply that is
|
|
1502
|
+
* small, true, and independent of the budget.
|
|
1503
|
+
*/
|
|
1504
|
+
function enforceOutlineBudget(res, requested) {
|
|
1505
|
+
if (typeof requested !== "number")
|
|
1506
|
+
return res;
|
|
1507
|
+
const payload = parseToolResultJson(res);
|
|
1508
|
+
if (!payload)
|
|
1509
|
+
return res;
|
|
1510
|
+
// A panel that supports the budget echoes it back and has already applied its
|
|
1511
|
+
// own ladder. Nothing to enforce, and second-guessing it would be wrong.
|
|
1512
|
+
if (typeof payload.max_chars === "number")
|
|
1513
|
+
return res;
|
|
1514
|
+
const outline = payload.outline;
|
|
1515
|
+
if (typeof outline !== "string")
|
|
1516
|
+
return res;
|
|
1517
|
+
// Within the bound already. The panel ignored the parameter, but the reply
|
|
1518
|
+
// honours it, and refusing a reply that FITS would be a bound in name only.
|
|
1519
|
+
if (outline.length <= requested)
|
|
1520
|
+
return res;
|
|
1521
|
+
const idx = res.content.findIndex((c) => c.type === "text");
|
|
1522
|
+
if (idx < 0)
|
|
1523
|
+
return res;
|
|
1524
|
+
const nodes = typeof payload.node_count === "number" ? payload.node_count : null;
|
|
1525
|
+
const groups = typeof payload.group_count === "number" ? payload.group_count : null;
|
|
1526
|
+
const shape = nodes != null
|
|
1527
|
+
? `The graph has ${nodes} node(s)${groups != null ? ` and ${groups} group(s)` : ""}.`
|
|
1528
|
+
: "";
|
|
1529
|
+
// The refusal goes IN the outline field, not an empty string (codex review).
|
|
1530
|
+
// Two reasons. It is what the panel itself does at its own floor, so a caller
|
|
1531
|
+
// that handles one refusal handles both. And an empty outline next to
|
|
1532
|
+
// `node_count: 137` is exactly the contradiction #1184 was about — a consumer
|
|
1533
|
+
// that reads `outline` and finds nothing has been told the canvas is empty by
|
|
1534
|
+
// a reply whose own counts say otherwise. Bounded and fixed-length, so it
|
|
1535
|
+
// cannot itself overrun a budget whose floor is OUTLINE_MAX_CHARS_FLOOR.
|
|
1536
|
+
payload.outline =
|
|
1537
|
+
`NO OUTLINE — this panel build ignores \`max_chars\`, and its full reply (${outline.length} chars) exceeds the bound of ${requested} you set. ` +
|
|
1538
|
+
`It is withheld rather than cut, because a truncated outline would read as a complete one. ` +
|
|
1539
|
+
`${shape} See \`max_chars_hint\` for what to do.`.trim();
|
|
1540
|
+
payload.detail_level = "refused";
|
|
1541
|
+
payload.degraded = true;
|
|
1542
|
+
payload.budget_applied_by = "orchestrator";
|
|
1543
|
+
payload.unbounded_chars = outline.length;
|
|
1544
|
+
payload.degraded_reason =
|
|
1545
|
+
`This panel build ignores \`max_chars\`, so it returned the FULL outline (${outline.length} chars) against a bound of ${requested}. ` +
|
|
1546
|
+
`It is withheld rather than cut: a truncated outline would read as a complete one. ${shape}`.trim();
|
|
1547
|
+
return {
|
|
1548
|
+
...res,
|
|
1549
|
+
content: res.content.map((c, i) => i === idx && c.type === "text" ? { ...c, text: JSON.stringify(payload, null, 2) } : c),
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
/** The three levers when the orchestrator had to withhold an outline (#1203),
|
|
1553
|
+
* in the order worth trying. Shared so the two riders cannot drift. */
|
|
1554
|
+
function outlineBudgetRemedies(unbounded) {
|
|
1555
|
+
// Every "raise it" must name the ceiling, or a caller already at the maximum
|
|
1556
|
+
// is sent on a retry that cannot change anything (the repo-wide remedy check,
|
|
1557
|
+
// which caught this text before it shipped).
|
|
1558
|
+
// The ceiling has to sit next to the parameter name, not at the end of the
|
|
1559
|
+
// sentence: the remedy check reads "raise `X` … up to N" as one phrase, and it
|
|
1560
|
+
// is right to — a caller skimming for whether the retry is possible should not
|
|
1561
|
+
// have to finish the clause.
|
|
1562
|
+
const raise = unbounded == null
|
|
1563
|
+
? `Raise \`max_chars\` up to ${OUTLINE_MAX_CHARS_CEILING}`
|
|
1564
|
+
: unbounded <= OUTLINE_MAX_CHARS_CEILING
|
|
1565
|
+
? `Raise \`max_chars\` up to ${OUTLINE_MAX_CHARS_CEILING} — this outline needs at least ${unbounded} to be accepted whole`
|
|
1566
|
+
: `Raising \`max_chars\` will NOT help — the full outline is past its ceiling of ${OUTLINE_MAX_CHARS_CEILING}`;
|
|
1567
|
+
return (`${raise}; update the ComfyUI Agent Panel, which can shed per-node detail and still cover every node inside a smaller bound; ` +
|
|
1568
|
+
`or scope the read with panel_query_graph {ids:[…], fields:'detail'}.`);
|
|
1569
|
+
}
|
|
1435
1570
|
function markBudgetIgnored(res, requested) {
|
|
1436
1571
|
if (typeof requested !== "number")
|
|
1437
1572
|
return res;
|
|
@@ -5767,8 +5902,11 @@ export function buildPanelToolDefs() {
|
|
|
5767
5902
|
// The synthetic `__budget_ignored` flag below is derived from the reply, not
|
|
5768
5903
|
// sent by the panel: a build that supports the budget echoes `max_chars` back.
|
|
5769
5904
|
markBudgetIgnored(
|
|
5905
|
+
// #1203 — and ENFORCE it here when the panel could not, before the
|
|
5906
|
+
// unbounded outline reaches the caller who asked for a bound.
|
|
5907
|
+
enforceOutlineBudget(
|
|
5770
5908
|
// #1184 — an empty outline is re-verified before it is believed.
|
|
5771
|
-
await confirmEmptyOutline(ctx, await ctx.call({ cmd: "graph_outline", max_chars: args.max_chars }), () => ctx.call({ cmd: "graph_outline", max_chars: args.max_chars })), args.max_chars), [
|
|
5909
|
+
await confirmEmptyOutline(ctx, await ctx.call({ cmd: "graph_outline", max_chars: args.max_chars }), () => ctx.call({ cmd: "graph_outline", max_chars: args.max_chars })), args.max_chars), args.max_chars), [
|
|
5772
5910
|
{
|
|
5773
5911
|
// #809 (codex gate): a panel older than this budget IGNORES `max_chars` and
|
|
5774
5912
|
// returns the full outline, so the bound this tool advertises silently did
|
|
@@ -5777,8 +5915,24 @@ export function buildPanelToolDefs() {
|
|
|
5777
5915
|
// unbounded reply as "this fitted".
|
|
5778
5916
|
flag: "__budget_ignored",
|
|
5779
5917
|
key: "max_chars_hint",
|
|
5780
|
-
text: (p) =>
|
|
5781
|
-
|
|
5918
|
+
text: (p) => {
|
|
5919
|
+
const asked = typeof args.max_chars === "number" ? args.max_chars : OUTLINE_MAX_CHARS_DEFAULT;
|
|
5920
|
+
const nodes = typeof p.node_count === "number" ? p.node_count : "all";
|
|
5921
|
+
// #1203 — three different situations used to share one sentence
|
|
5922
|
+
// that described only the worst of them. Saying "the outline
|
|
5923
|
+
// below is the full, unbounded one" when it was withheld, or
|
|
5924
|
+
// when it fitted anyway, is its own false report.
|
|
5925
|
+
if (p.budget_applied_by === "orchestrator") {
|
|
5926
|
+
const unbounded = typeof p.unbounded_chars === "number" ? p.unbounded_chars : null;
|
|
5927
|
+
return (`This panel build does not support \`max_chars\`, so it returned the FULL outline for all ${nodes} node(s)` +
|
|
5928
|
+
`${unbounded != null ? ` (${unbounded} chars)` : ""} against your bound of ${asked}. ` +
|
|
5929
|
+
`NO outline is included: it is withheld rather than cut, because a truncated outline would read as a complete one. ` +
|
|
5930
|
+
outlineBudgetRemedies(unbounded));
|
|
5931
|
+
}
|
|
5932
|
+
return (`This panel build does not support \`max_chars\`, so the budget you set (${asked}) was not applied by the panel — ` +
|
|
5933
|
+
`the outline below is its full, unbounded reply for all ${nodes} node(s), which happens to fit within your bound. ` +
|
|
5934
|
+
`Update the ComfyUI Agent Panel to have the budget applied properly; on a larger graph an unbounded reply will not fit.`);
|
|
5935
|
+
},
|
|
5782
5936
|
},
|
|
5783
5937
|
{
|
|
5784
5938
|
// On an older panel this is never set either, so the rider is inert there.
|
|
@@ -5797,6 +5951,21 @@ export function buildPanelToolDefs() {
|
|
|
5797
5951
|
// it "still covers ALL nodes" would describe content the reader cannot
|
|
5798
5952
|
// see, which is the same lie as a silent cut.
|
|
5799
5953
|
if (p.detail_level === "refused") {
|
|
5954
|
+
// #1203 — an ORCHESTRATOR refusal is a different situation and
|
|
5955
|
+
// needs different advice. The panel refuses because even its
|
|
5956
|
+
// smallest whole-graph form did not fit, so the question is
|
|
5957
|
+
// whether raising the bound can ever help. Here the panel never
|
|
5958
|
+
// applied the bound at all: the full outline's size is known
|
|
5959
|
+
// exactly, updating the panel unlocks the shed ladder, and the
|
|
5960
|
+
// generic text's "this build does not report how large the
|
|
5961
|
+
// smallest form would be" would send the reader chasing a
|
|
5962
|
+
// capability the reply is already explaining is absent.
|
|
5963
|
+
if (p.budget_applied_by === "orchestrator") {
|
|
5964
|
+
const unbounded = typeof p.unbounded_chars === "number" ? p.unbounded_chars : null;
|
|
5965
|
+
return (`NO outline was returned: this panel build ignores \`max_chars\`, so its reply${unbounded != null ? ` (${unbounded} chars)` : ""} exceeded \`max_chars\`=${inForce} and a PARTIAL outline is deliberately withheld because it would read as complete. ` +
|
|
5966
|
+
`The graph has ${nodes} node(s) and ${groups} group(s). ` +
|
|
5967
|
+
outlineBudgetRemedies(unbounded));
|
|
5968
|
+
}
|
|
5800
5969
|
// And if the floor exceeds the CEILING, raising is a guaranteed second
|
|
5801
5970
|
// refusal — a dead retry inside the message explaining the first.
|
|
5802
5971
|
//
|
|
@@ -7970,6 +8139,9 @@ export function buildPanelToolDefs() {
|
|
|
7970
8139
|
const fallback = restartTimeoutFallbackAdvice({
|
|
7971
8140
|
headlessBase: getComfyUIBaseUrl(),
|
|
7972
8141
|
panelBase: captureRebootHealthBase(ctx),
|
|
8142
|
+
// #1233 — the raw observed origin, so a CONFIRMED mismatch is not lost inside
|
|
8143
|
+
// the proof's null. Server-observed on the WS upgrade; page JS cannot forge it.
|
|
8144
|
+
observedOrigin: ctx.bridge?.tabServerOrigin?.(ctx.tabId) ?? null,
|
|
7973
8145
|
});
|
|
7974
8146
|
return ok(`No confirmation received within ${Math.round(confirmBudget / 1000)}s, so I did NOT ` +
|
|
7975
8147
|
"restart ComfyUI. The panel tab may be backgrounded or still reconnecting after a " +
|