UNPKG

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
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