comfyui-mcp 0.52.35 → 0.52.36

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.
@@ -1460,16 +1460,7 @@ export function classifyTabReconnect({ serverReady, baselineCaptured, tabBack, }
1460
1460
  export function classifyRestartFallbackTarget({ headlessBase, panelBase, observedOrigin = null, }) {
1461
1461
  if (panelBase != null && sameHttpBase(headlessBase, panelBase))
1462
1462
  return { kind: "same" };
1463
- const dnsAmbiguous = (u) => {
1464
- if (!u)
1465
- return false;
1466
- try {
1467
- return new URL(u).hostname.toLowerCase().replace(/^\[|\]$/g, "") === "localhost";
1468
- }
1469
- catch {
1470
- return false;
1471
- }
1472
- };
1463
+ const dnsAmbiguous = isDnsAmbiguousLoopback;
1473
1464
  const originMismatch = observedOrigin != null &&
1474
1465
  !dnsAmbiguous(observedOrigin) &&
1475
1466
  !dnsAmbiguous(headlessBase) &&
@@ -1597,7 +1588,11 @@ function restartRefusedPreservingBinding(ctx, note) {
1597
1588
  note,
1598
1589
  });
1599
1590
  }
1600
- function unboundLocalRestartRefusalNote(ctx, panelBase) {
1591
+ function unboundLocalRestartRefusalNote(ctx, panelBase,
1592
+ // panel#1546 — the blocker from the SAME capture that produced `panelBase`.
1593
+ // Passed in rather than re-derived: a second call to the classifier could read
1594
+ // a binding that changed in between and explain a refusal that did not happen.
1595
+ blocker = null) {
1601
1596
  return ("Refusing to restart ComfyUI: I could not confirm that this panel's ComfyUI is " +
1602
1597
  "the local instance I can account for, so I cannot tell which server the restart " +
1603
1598
  "would stop — and it STOPS it, relying on whatever supervises it to start it " +
@@ -1623,6 +1618,10 @@ function unboundLocalRestartRefusalNote(ctx, panelBase) {
1623
1618
  panelBase,
1624
1619
  observedOrigin: ctx.bridge?.tabServerOrigin?.(ctx.tabId) ?? null,
1625
1620
  }) +
1621
+ // panel#1546 — and say WHICH proof was missing. Without this, a `localhost`
1622
+ // origin (curable in one step), an absent Origin (cured by updating the
1623
+ // panel) and a relayed tab (not curable here at all) were the same sentence.
1624
+ restartIdentityBlockerNote(blocker) +
1626
1625
  // artokun/comfyui-mcp-panel#769 — a REMOTE ComfyUI reached over a
1627
1626
  // tunnel or port-forward has a LOOPBACK host, and remoteUrlActive is
1628
1627
  // derived from exactly that (`forceRemote || !isLoopbackHost(host)`).
@@ -1848,16 +1847,28 @@ function objectInfoHostMismatchMessage(err) {
1848
1847
 
1849
1848
  Node definitions are read from COMFYUI_URL (${configured}) for a pack/path/inline source. That host did not answer /object_info.`;
1850
1849
  }
1851
- function captureRebootHealthBase(ctx) {
1850
+ /** The SINGLE decision. `captureRebootHealthBase` returns `.base`; the refusal
1851
+ * note reads `.blocker`. Two readers of one evidence set that cannot disagree —
1852
+ * the same reason `classifyRestartFallbackTarget` was extracted for #1593. */
1853
+ function resolveRebootHealthBinding(ctx) {
1854
+ const no = (blocker) => ({ base: null, blocker });
1852
1855
  if (isCloudMode() || isRemoteMode())
1853
- return null;
1856
+ return no({ kind: "not-local-mode" });
1854
1857
  const bootBase = getBootLocalComfyUIBaseUrl(); // server-authorized, hello-immutable
1855
- if (!bootBase || !isLoopbackOrigin(bootBase))
1856
- return null;
1858
+ if (!bootBase)
1859
+ return no({ kind: "no-boot-base" });
1860
+ // `localhost` fails isLoopbackOrigin (loopbackFamily returns null for it by
1861
+ // design), so it lands here — NOT in the origin comparison below, which never
1862
+ // sees an ambiguous boot base. Naming it separately is the difference between
1863
+ // "your ComfyUI is not on this machine" (false) and a config line to change.
1864
+ if (isDnsAmbiguousLoopback(bootBase))
1865
+ return no({ kind: "boot-base-dns-ambiguous", bootBase });
1866
+ if (!isLoopbackOrigin(bootBase))
1867
+ return no({ kind: "boot-base-not-loopback", bootBase });
1857
1868
  const base = bootBase.replace(/\/+$/, "");
1858
1869
  // Server-trusted provenance: the tab arrived on the token-less loopback listener.
1859
1870
  if (ctx.bridge?.tabIsLocal?.(ctx.tabId) !== true)
1860
- return null;
1871
+ return no({ kind: "tab-not-local" });
1861
1872
  // And the rebooted tab must provably front THAT SAME boot instance. Use the SERVER-
1862
1873
  // OBSERVED handshake Origin (tabServerOrigin) — the browser sets it on the WS upgrade
1863
1874
  // and blocks page JS from forging it — NOT the spoofable client hello.comfyui_url
@@ -1874,13 +1885,143 @@ function captureRebootHealthBase(ctx) {
1874
1885
  // (loopbackFamily → null), so it never matches a concrete literal and this returns null
1875
1886
  // (coordinator P0). A different instance / family / path / absent Origin → null too.
1876
1887
  const origin = ctx.bridge?.tabServerOrigin?.(ctx.tabId);
1877
- if (!sameHttpBase(origin, base))
1878
- return null;
1888
+ if (!sameHttpBase(origin, base)) {
1889
+ // SAME verdict as before — only the reason is now carried out. Ordered so each
1890
+ // kind names the FIRST thing that made a match impossible, because that is the
1891
+ // one the reader can act on.
1892
+ if (!origin)
1893
+ return no({ kind: "origin-absent", bootBase: base });
1894
+ return no(classifyOriginBlocker(origin, base));
1895
+ }
1879
1896
  // Return a CONNECTABLE probe URL bound to the SAME concrete family identity matched
1880
1897
  // above: a wildcard-bound (0.0.0.0/::) local ComfyUI is reachable on loopback, so probe
1881
1898
  // the family literal at that port (127.0.0.1 / [::1]). The probe (and the auth headers
1882
1899
  // it carries) can therefore never cross to a different-family instance.
1883
- return loopbackProbeUrl(base);
1900
+ return { base: loopbackProbeUrl(base), blocker: null };
1901
+ }
1902
+ function captureRebootHealthBase(ctx) {
1903
+ return resolveRebootHealthBinding(ctx).base;
1904
+ }
1905
+ /**
1906
+ * panel#1546 — WHY a present Origin failed to match the boot base.
1907
+ *
1908
+ * Ordered so each verdict names the FIRST thing that made a match impossible, and
1909
+ * NEVER blames the DNS ambiguity for something the ambiguity did not cause. A
1910
+ * `localhost` origin on a DIFFERENT PORT is a different server and is reported as
1911
+ * one; a `localhost` origin against a basePath mount is unprovable even with the
1912
+ * host resolved, so it stays the path verdict. Only the case where resolving
1913
+ * `localhost` would ACTUALLY settle it — same port, no mount, exactly one
1914
+ * ambiguous side — gets the DNS answer and its one-step remedy.
1915
+ *
1916
+ * Exported for test: the boot base is fixed at process start, so the
1917
+ * boot-side-ambiguous direction has no handler-driven fixture.
1918
+ */
1919
+ export function classifyOriginBlocker(origin, bootBase) {
1920
+ // Callers reach here only past `isLoopbackOrigin(bootBase)`, which rejects
1921
+ // `localhost` — so the boot base is always a concrete literal and the panel is
1922
+ // the only side that can be ambiguous. Asserted, not assumed: an ambiguous boot
1923
+ // base would make the comparison below meaningless rather than merely wrong.
1924
+ if (isDnsAmbiguousLoopback(origin) && !isDnsAmbiguousLoopback(bootBase)) {
1925
+ let sameSpot = false;
1926
+ try {
1927
+ const o = new URL(origin);
1928
+ const b = new URL(bootBase);
1929
+ // A handshake Origin is pathless BY CONSTRUCTION — normalizeHandshakeOrigin
1930
+ // rebuilds it from protocol/hostname/port and relayIdentityOrigin uses
1931
+ // URL.origin — so only the BOOT base can carry a mount, and the DNS answer
1932
+ // can never settle one. Ports must already agree; resolving a hostname never
1933
+ // reconciles :8188 with :8189. Compare EFFECTIVE ports: the same normalizer
1934
+ // fills in 80/443 explicitly, so a default-port boot base ("") would
1935
+ // otherwise read as a port mismatch against an origin that says "80".
1936
+ const port = (u) => u.port || (u.protocol === "https:" ? "443" : "80");
1937
+ sameSpot =
1938
+ o.protocol === b.protocol &&
1939
+ port(o) === port(b) &&
1940
+ b.pathname.replace(/\/+$/, "") === "";
1941
+ }
1942
+ catch {
1943
+ sameSpot = false;
1944
+ }
1945
+ if (sameSpot)
1946
+ return { kind: "origin-dns-ambiguous", bootBase, origin };
1947
+ }
1948
+ // The Origin is pathless by construction, so a boot base under a basePath mount
1949
+ // can be same-ORIGIN yet unprovable as the same INSTANCE. Distinguish that from a
1950
+ // genuinely different host:port, which is the #851 wrong-server case.
1951
+ if (sameHttpOrigin(origin, bootBase))
1952
+ return { kind: "origin-path-ambiguous", bootBase, origin };
1953
+ return { kind: "origin-different", bootBase, origin };
1954
+ }
1955
+ /** True when a URL's host is the DNS-ambiguous `localhost` — it names a loopback
1956
+ * service without revealing which family the browser actually reached, which is
1957
+ * exactly why `loopbackFamily` refuses it (coordinator P0, #1233). Shared by the
1958
+ * blocker classifier and `classifyRestartFallbackTarget` so "ambiguous" means one
1959
+ * thing in this file. */
1960
+ function isDnsAmbiguousLoopback(rawUrl) {
1961
+ if (!rawUrl)
1962
+ return false;
1963
+ try {
1964
+ return new URL(rawUrl).hostname.toLowerCase().replace(/^\[|\]$/g, "") === "localhost";
1965
+ }
1966
+ catch {
1967
+ return false;
1968
+ }
1969
+ }
1970
+ /**
1971
+ * panel#1546 — the one sentence that turns the refusal from a dead end into a step.
1972
+ *
1973
+ * The reporter's panel was attached, local and graph-tool ready; the refusal named
1974
+ * neither what was missing nor how to supply it, so the filed diagnosis was
1975
+ * "target identity cannot be confirmed" with no next action. Each blocker has a
1976
+ * DIFFERENT next action, and `origin-dns-ambiguous` — a browser at
1977
+ * `localhost:8188` against a `127.0.0.1:8188` boot base, the ordinary local setup
1978
+ * this file's own comments already flag as returning null — has a one-step one.
1979
+ *
1980
+ * Deliberately NOT a fix for the underlying ambiguity. Proving that a `localhost`
1981
+ * tab reached the boot family would mean trusting a resolution this process did
1982
+ * not observe, and #1233 recorded why that is a P0. Naming the cause costs no
1983
+ * soundness; guessing it would.
1984
+ */
1985
+ export function restartIdentityBlockerNote(blocker) {
1986
+ if (!blocker)
1987
+ return "";
1988
+ const why = " WHY I could not confirm it: ";
1989
+ switch (blocker.kind) {
1990
+ case "not-local-mode":
1991
+ return `${why}this target is configured as remote/cloud, so there is no local process here to account for.`;
1992
+ case "no-boot-base":
1993
+ return `${why}no local ComfyUI address was resolvable when this server started, so there is no boot instance to compare against.`;
1994
+ case "boot-base-not-loopback":
1995
+ return (`${why}the ComfyUI this server booted against (${blocker.bootBase}) is not a loopback address, ` +
1996
+ "so it is not a process on this machine that I can account for.");
1997
+ case "boot-base-dns-ambiguous":
1998
+ return (`${why}this server is configured with COMFYUI_URL=${blocker.bootBase}. That IS loopback, but ` +
1999
+ `"localhost" does not say which address it resolves to — 127.0.0.1 and ::1 can be DIFFERENT ` +
2000
+ "ComfyUI instances on the same port — so I cannot tie it to a specific local process, and no " +
2001
+ "panel tab can be matched against it. This disables the panel restart for every tab, not just " +
2002
+ "this one. FIX: set COMFYUI_URL to the literal your ComfyUI actually listens on " +
2003
+ "(http://127.0.0.1:<port>, or http://[::1]:<port> for an IPv6-only bind) and restart this server.");
2004
+ case "tab-not-local":
2005
+ return (`${why}this panel tab did not arrive on the local loopback listener — it reached me over a relay, ` +
2006
+ "tunnel, LAN address or pairing link, so I cannot treat the ComfyUI behind it as one of mine. " +
2007
+ "Restart it from the machine it actually runs on.");
2008
+ case "origin-absent":
2009
+ return (`${why}this tab's WebSocket handshake carried no Origin, so there is nothing to check against ` +
2010
+ `the boot instance (${blocker.bootBase}). An older panel build does this; updating the panel restores the proof.`);
2011
+ case "origin-dns-ambiguous":
2012
+ return (`${why}this panel is open at ${blocker.origin}, and "localhost" does not say which loopback ` +
2013
+ `address the browser actually reached — it can be 127.0.0.1 or ::1, and those can be DIFFERENT ` +
2014
+ `ComfyUI instances on the same port. The boot instance I account for is ${blocker.bootBase}, so ` +
2015
+ `I cannot show the two are one server. FIX: open the panel at ${loopbackProbeUrl(blocker.bootBase)} ` +
2016
+ `— the same ComfyUI, addressed by its literal.`);
2017
+ case "origin-path-ambiguous":
2018
+ return (`${why}the boot instance is mounted under a path (${blocker.bootBase}) and a handshake Origin ` +
2019
+ `carries no path (${blocker.origin}), so this tab could be fronting a different ComfyUI at the ` +
2020
+ "same host and port. I will not stop a server on that.");
2021
+ case "origin-different":
2022
+ return (`${why}this panel is open at ${blocker.origin}, which is not the ComfyUI this server booted ` +
2023
+ `against (${blocker.bootBase}) — a different server, so stopping mine would not restart yours.`);
2024
+ }
1884
2025
  }
1885
2026
  /**
1886
2027
  * #1671 — the configured LOCAL boot instance, when that is a known loopback
@@ -13238,7 +13379,8 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
13238
13379
  // of the current binding: a silent rebind here would confirm a restart of
13239
13380
  // a tab the user was not working in. Healing belongs on the refusal path.
13240
13381
  if (!isRemoteMode() && !isCloudMode()) {
13241
- const identityHealthBase = captureRebootHealthBase(ctx);
13382
+ const identityBinding = resolveRebootHealthBinding(ctx);
13383
+ const identityHealthBase = identityBinding.base;
13242
13384
  const identityBound = identityHealthBase != null &&
13243
13385
  sameHttpBase(getComfyUIBaseUrl(), identityHealthBase);
13244
13386
  if (!identityBound) {
@@ -13246,7 +13388,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
13246
13388
  ? ctx.bridge.canReach(ctx.tabId) === true
13247
13389
  : true;
13248
13390
  if (tabStillHere) {
13249
- return restartRefusedPreservingBinding(ctx, unboundLocalRestartRefusalNote(ctx, identityHealthBase));
13391
+ return restartRefusedPreservingBinding(ctx, unboundLocalRestartRefusalNote(ctx, identityHealthBase, identityBinding.blocker));
13250
13392
  }
13251
13393
  // Tab is gone: confirm will be unreachable and #1671 crash-recovery
13252
13394
  // decides whether a headless restart can still be accounted for.
@@ -13876,7 +14018,8 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
13876
14018
  // can be proven. They keep restart_comfyui, and the note says so. Weighed
13877
14019
  // against #814 — where exactly such a user's server was stopped and never came
13878
14020
  // back — declining is the recoverable side.
13879
- const preflightHealthBase = captureRebootHealthBase(ctx);
14021
+ const preflightBinding = resolveRebootHealthBinding(ctx);
14022
+ const preflightHealthBase = preflightBinding.base;
13880
14023
  const preflightBound = preflightHealthBase != null &&
13881
14024
  sameHttpBase(getComfyUIBaseUrl(), preflightHealthBase);
13882
14025
  // #848: what the instance was OBSERVED running with, taken from the preflight
@@ -13903,7 +14046,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
13903
14046
  // Identity was proven before the card; landing here means the binding
13904
14047
  // was lost during the confirm wait. Same refusal, and the tab that is
13905
14048
  // still here must stay usable (#1819).
13906
- return restartRefusedPreservingBinding(ctx, unboundLocalRestartRefusalNote(ctx, preflightHealthBase));
14049
+ return restartRefusedPreservingBinding(ctx, unboundLocalRestartRefusalNote(ctx, preflightHealthBase, preflightBinding.blocker));
13907
14050
  }
13908
14051
  if (preflightBound) {
13909
14052
  // Snapshot the target GENERATION at the decision (r11): a final-state