comfyui-mcp
Version:
Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),
145 lines • 4.72 kB
JavaScript
import { EventEmitter } from "node:events";
import WebSocket from "ws";
import { describeFetchFailure } from "../utils/errors.js";
import { formatComfyUIUrl } from "./comfyui-url.js";
function isConnectionRefused(error) {
return describeFetchFailure(error).code === "ECONNREFUSED";
}
/**
* A Node ws-compatible socket that preserves a literal loopback target for
* the first dial and retries only a refused 127.0.0.1 connection at localhost.
* The retry is safe because ECONNREFUSED proves the first handshake reached no
* server; resets and timeouts are deliberately surfaced without re-issuing.
*
* The stable-canvas client consumes the DOM-style addEventListener surface,
* while the MCP's direct monitors consume ws's EventEmitter surface. This
* adapter exposes both and keeps the underlying socket private so a failed
* first attempt cannot leak an error or close event before the fallback.
*/
export class LoopbackWebSocket extends EventEmitter {
static CONNECTING = WebSocket.CONNECTING;
static OPEN = WebSocket.OPEN;
static CLOSING = WebSocket.CLOSING;
static CLOSED = WebSocket.CLOSED;
socket;
literalUrl;
fallbackTried = false;
callerClosed = false;
options;
domListeners = new Map();
constructor(url, options) {
super();
const literalUrl = String(url);
this.literalUrl = literalUrl;
this.options = options;
this.socket = this.open(literalUrl);
}
open(url) {
const socket = this.options && typeof this.options === "object" && !Array.isArray(this.options)
? new WebSocket(url, this.options)
: new WebSocket(url, this.options);
socket.on("open", () => {
if (socket !== this.socket)
return;
this.emit("open");
});
socket.on("message", (data, isBinary) => {
if (socket !== this.socket)
return;
this.emit("message", data, isBinary);
});
socket.on("error", (error) => {
if (socket !== this.socket)
return;
if (!this.callerClosed &&
!this.fallbackTried &&
formatComfyUIUrl(this.literalUrl) !== url &&
isConnectionRefused(error)) {
this.fallbackTried = true;
const fallback = this.open(formatComfyUIUrl(this.literalUrl));
this.socket = fallback;
socket.terminate();
return;
}
this.emit("error", error);
});
socket.on("close", (code, reason) => {
if (socket !== this.socket)
return;
this.emit("close", code, reason);
});
return socket;
}
get binaryType() {
return this.socket.binaryType;
}
get CONNECTING() {
return WebSocket.CONNECTING;
}
get OPEN() {
return WebSocket.OPEN;
}
get CLOSING() {
return WebSocket.CLOSING;
}
get CLOSED() {
return WebSocket.CLOSED;
}
set binaryType(value) {
this.socket.binaryType = value;
}
get bufferedAmount() {
return this.socket.bufferedAmount;
}
get extensions() {
return this.socket.extensions;
}
get protocol() {
return this.socket.protocol;
}
get readyState() {
return this.socket.readyState;
}
get url() {
return this.socket.url;
}
addEventListener(type, listener) {
let listeners = this.domListeners.get(type);
if (!listeners) {
listeners = new Map();
this.domListeners.set(type, listeners);
}
if (listeners.has(listener))
return;
const adapted = (...args) => {
const event = type === "message"
? { data: args[0] }
: type === "close"
? { code: args[0], reason: args[1] }
: args[0];
listener.call(this, event);
};
listeners.set(listener, adapted);
this.on(type, adapted);
}
removeEventListener(type, listener) {
const listeners = this.domListeners.get(type);
const adapted = listeners?.get(listener);
if (!adapted)
return;
this.removeListener(type, adapted);
listeners?.delete(listener);
}
send(data, ...args) {
this.socket.send(data, ...args);
}
close(code, reason) {
this.callerClosed = true;
this.socket.close(code, reason);
}
terminate() {
this.callerClosed = true;
this.socket.terminate();
}
}
//# sourceMappingURL=loopback-websocket.js.map